/******************************************************************************
*
* Copyright 2011, Cypress Semiconductor Corporation.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*   1. Redistributions of source code must retain the above copyright notice,
*      this list of conditions and the following disclaimer.
*
*   2. Redistributions in binary form must reproduce the above copyright notice,
*      this list of conditions and the following disclaimer in the documentation
*      and/or other materials provided with the distribution.
*
*   3. Neither the name of the Cypress Semiconductor Corporation, nor the names
*      of its contributors may be used to endorse or promote products derived
*      from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY Cypress Semiconductor Corporation ''AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Cypress Semiconductor Corporation
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are
* those of the authors and should not be interpreted as representing official
* policies, either expressed or implied, of Cypress Semiconductor Corporation.
*
*******************************************************************************
*
* Authors:
*       Dudley Du <dudl@cypress.com>
* Usage:
*       Used to update Cypress Trackpad device firmware image from .iic or
*       .bin files.
*       e.g., update Cypress Trackpad device with new "new_image.iic" file:
*               $ sudo ./cyapa_fw_update new_image.iic -f
* Version:
*       1.0.0   2011/07/26      Initial release of cyapa_fw_update utility tool.
*/

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>
#include <linux/limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include "cyapa.h"

#ifdef DBG
#define prt_info(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define prt_warn(fmt, ...) printf("WARNING: " fmt, ##__VA_ARGS__)
#define prt_err(fmt, ...) printf("ERROR: " fmt, ##__VA_ARGS__)
#define prt_dbg(fmt, ...) printf("DEBUG: %d: " fmt, __LINE__, ##__VA_ARGS__)
#define DBG_DUMP_DATA_BLOCK 1
#else
#define prt_info(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define prt_warn(fmt, ...) printf("WARNING: " fmt, ##__VA_ARGS__)
#define prt_err(fmt, ...) printf("ERROR: " fmt, ##__VA_ARGS__)
#define prt_dbg(fmt, ...)
#define DBG_DUMP_DATA_BLOCK 0
#endif

#define CYAPA_FW_UPDATE_VER  "1.0.0"

#define DEFAULT_PROGRAM_NAME "cyapa_fw_update"
#define DEFAULT_FW_BAK_FOLDER "/tmp/cypress"
#define DEFAULT_FW_BAK_IMAGE_NAME "/tmp/cypress/cyapa_bak_firmware.bin"

#define FILE_TYPE_IIC  0
#define FILE_TYPE_BIN  1

#define CYAPA_FW_OFFSET_START       0x0780
#define CYAPA_FW_CHECKSUM_END       0x07FF
#define CYAPA_FW_OFFSET_END         0x7FFF
#define CYAPA_FW_SIZE               (CYAPA_FW_OFFSET_END - \
                                     CYAPA_FW_OFFSET_START + 1)
#define CYAPA_BAK_READ_BLOCK_LEN    16
#define CYAPA_FW_BLOCK_LEN          64
#define CYAPA_FW_BLOCK_COUNT        (CYAPA_FW_SIZE / CYAPA_FW_BLOCK_LEN)
#define CYAPA_FW_START_BLOCK       (CYAPA_FW_OFFSET_START / CYAPA_FW_BLOCK_LEN)

#define CYAPA_IIC_CMD_OP_NONE	0
#define CYAPA_IIC_CMD_OP_READ	1
#define CYAPA_IIC_CMD_OP_WRITE  2
#define CYAPA_IIC_CMD_OP_DELAY  3
#define CYAPA_IIC_CMD_OP_tries_WRITE  4

/*
 * CYAPA firmware starts at an absolute offset.
 * We remove this offset when converting between firmware address and binary
 * file locations.
 */
#define CYAPA_FW_TO_BIN(x)  ((x) - CYAPA_FW_OFFSET_START)
#define CYAPA_BIN_TO_FW(x)  ((x) + CYAPA_FW_OFFSET_START)

#define CYAPA_IDAC_0_START  0x7900
#define CYAPA_IDAC_0_END    0x797F
#define CYAPA_IDAC_0_SIZE   (CYAPA_IDAC_0_END - CYAPA_IDAC_0_START + 1)
#define CYAPA_IDAC_0_DEF    0x08

#define CYAPA_IDAC_1_START  0x7A00
#define CYAPA_IDAC_1_END    0x7A7F
#define CYAPA_IDAC_1_SIZE   (CYAPA_IDAC_1_END - CYAPA_IDAC_1_START + 1)
#define CYAPA_IDAC_1_DEF    0x08

#define CYAPA_IDAC_2_START  0x7B00
#define CYAPA_IDAC_2_END    0x7BFF
#define CYAPA_IDAC_2_SIZE   (CYAPA_IDAC_2_END - CYAPA_IDAC_2_START + 1)
#define CYAPA_IDAC_2_DEF    0x90


struct args {
	/* the value should be FILE_TYPE_IIC or FILE_TYPE_BIN. */
	int file_type;
	bool backup_fw;
	bool force;
	bool convert;

	const char *new_fw_image;
	const char *bak_fw_image;

	int fd_dev;
	int fd_new_fw;
	int fd_bak_fw;
};

/*
 * command format:
 * write data:
 * byte 0 : operation.  CYAPA_IIC_CMD_OP_WRITE
 * byte 1 : length of behind real command data, from byte 2 to byte n.q
 * byte 2 - byte n : real command as show in .iic file.
 *       byte 2 : I2C address, 0x67 for trackpad.
 *       byte 3 : I2C registers offset where byte 4 -byte n should be written.
 *                usually 0x00 for trackpad.
 *       byte 4 - byte n : command data to be written to trackpad.
 *		byte 4 : index, 0x00, 0x10, 0x20, 0x30, 0x40.
 *
 * read status:
 * byte 0 : operation. CYAPA_IIC_CMD_OP_READ
 * byte 1 : read length.
 *
 * delay:
 * byte 0 : operation. CYAPA_IIC_CMD_OP_DELAY
 * byte 1 - byte 4 : delay milliseconds (int).
 */
struct cmds_update_block {
	int valid_cmds;
	unsigned char cmds[8][24];
};


/* Global buffer for storing firmware image */
unsigned char fw_buf[CYAPA_FW_SIZE];


#if DBG_DUMP_DATA_BLOCK
void cyapa_dump_data_block(const char *str, unsigned char *buf,
		unsigned int offset, int length)
{
	char strbuf[2048];
	unsigned int rest_len = sizeof(strbuf);
	char *p = strbuf;
	int i, len;

	memset(strbuf, 0, sizeof(strbuf));
	len = snprintf(p, rest_len, "%s: offset 0x%04X, %d bytes:",
			str, offset, length);
	rest_len -= len;
	p += len;
	for (i = 0; i < length && rest_len; i++, p += len, rest_len -= len) {
		len = snprintf(p, rest_len, " %02X",
				*(unsigned char *)(buf + i));
	}

	prt_info("%s\n", strbuf);
}

void cyapa_dump_delay_time(const char *str, int delay)
{
	prt_info("%s: delay [%d] ms\n", str, delay);
}
#else
void cyapa_dump_data_block(const char *str, unsigned char *buf,
		unsigned int offset, int length) {}
void cyapa_dump_delay_time(const char *str, int delay) {}
#endif

void show_usage(char *name)
{
	printf("Usage: %s <new-firmware-image> [options]\n",
		name ? name : DEFAULT_PROGRAM_NAME);
	printf("Options:\n");
	printf("\t -b, --backup\n");
	printf("\t     Backup current firmware before updating"
			" to new firmware.\n");
	printf("\t     If -o option is not set, the default back up"
			" path is \"/tmp/cypress\".\n");
	printf("\t     If -o option is set, use the path specified in"
			"-o <path>.\n");
	printf("\t -c, --convert <iic-path>\n");
	printf("\t     Convert new-firmware-image (.iic) to a .bin file.\n");
	printf("\t     Does not actually write firmware to the device.\n");
	printf("\t     Use --output to specify the destination path.\n");
	printf("\t -o, --output <path>\n");
	printf("\t     Specifies full path of the output file for where to\n");
	printf("\t     back up a copy of the current trackpad firmware.\n");
	printf("\t     Only valid when option -b is set and\n");
	printf("\t     filename must have a \'bin\' extension.\n");
	printf("\t     By default, without -o option, the back up path is:\n");
	printf("\t             %s\n", DEFAULT_FW_BAK_IMAGE_NAME);
	printf("\t     If the requested update fails, this backed up copy\n");
	printf("\t     will be rewritten to the trackpad.\n");
	printf("\t -f, --force\n");
	printf("\t     Force new firmware to be updated to trackpad device\n");
	printf("\t     and suppress any prompt information.\n");
	printf("\t -v, --version\n");
	printf("\t     Print version information and exit.\n");
	printf("\t -h, --help\n");
	printf("\t     Show this help information.\n");
	printf("NOTE:\n");
	printf("       This program must be executed as root\n");
	printf("       cyapa_fw_update program release version: %s\n\n",
			CYAPA_FW_UPDATE_VER);
}

void show_version_info(char *name)
{
	printf("Cypress utility: %s %s\n\n", name, CYAPA_FW_UPDATE_VER);
	printf("Copyright (C) 2011 Cypress Semiconductor Corporation.\n");
	printf("License BSD 2-Clause License or later.\n");
	printf("This is free software: you are free to change and"
		" redistribute it.\n");
	printf("There is NO WARRANTY, to the extent permitted by law.\n\n");
}

static struct option options[] = {
		{"backup", no_argument, NULL, 'b'},
		{"convert", no_argument, NULL, 'c'},
		{"force", no_argument, NULL, 'f'},
		{"help", no_argument, NULL, 'h'},
		{"output", required_argument, NULL, 'o'},
		{"version", no_argument, NULL, 'v'},
		{0, 0, 0, 0}
};


bool ends_with(const char* name, const char* ext)
{
	size_t nlen = strlen(name);
	size_t elen = strlen(ext);

	return (nlen >= elen && !strcmp(&name[nlen-elen], ext));
}


/**
 * return value:
 *     0 - parse and get valid input parameters.
 *     1 - show program usage information.
 *     2 - show program version information.
 *     < 0 - failed to parse and check input parameters.
 */
int check_input_args(int argc, char **argv, struct args *args)
{
	int c;

	while (1) {
		int index = 0;

		c = getopt_long(argc, argv, "bcfho:v", options, &index);
		if (c == -1)
			break;

		switch (c) {
		case 'b':
			args->backup_fw = true;
			break;
		case 'c':
			args->convert = true;
			break;
		case 'f':
			args->force = true;
			break;
		case 'o':
			args->bak_fw_image = optarg;
			break;
		case 'v':
			return 2;
		default:
			return 1;
		}
	}

	/* The last option should be the path of a new firmware image */
	if (optind < argc)
		args->new_fw_image = argv[optind++];

	if (args->new_fw_image) {
		if (ends_with(args->new_fw_image, ".iic"))
			args->file_type = FILE_TYPE_IIC;
		else if (ends_with(args->new_fw_image, ".bin"))
			args->file_type = FILE_TYPE_BIN;
		else
			return 1;
	}

	if (args->backup_fw && !args->bak_fw_image) {
		int fd;

		args->bak_fw_image = DEFAULT_FW_BAK_IMAGE_NAME;
		mkdir(DEFAULT_FW_BAK_FOLDER, 0777);
		remove(args->bak_fw_image);
		fd = creat(args->bak_fw_image, O_CREAT |
			O_TRUNC | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
		if (fd < 0)
			return 1;
		close(fd);
	}

	if (args->new_fw_image)
		prt_info("Update new firmware image from: %s\n",
			args->new_fw_image);
	if (args->backup_fw && args->backup_fw)
		prt_info("Backup firmware image from trackpad device to: %s\n",
			args->bak_fw_image);

	return 0;
}

#define MAX_PROGRESS_LENGTH 110
#define DEFAULT_TERM_WIDTH  80
int get_terminal_width(void)
{
	int ret;
	struct winsize sz;

	ret = ioctl(0, TIOCGWINSZ, &sz);
	if (ret < 0)
		return DEFAULT_TERM_WIDTH;
	else
		return (sz.ws_col > MAX_PROGRESS_LENGTH) ?
			MAX_PROGRESS_LENGTH :
			((sz.ws_col < 20) ? DEFAULT_TERM_WIDTH : sz.ws_col);
}

/**
* Output progress format:
* |-----------------------------             | ddd% |
*/
#define SHOW_PROGRESS_INIT  0
#define SHOW_PROGRESS_CONT  1
#define SHOW_PROGRESS_EXIT  2
void show_progress(int percent, int state, char *str)
{
	int i;
	int term_width;
	int progress_count;
	char buf[MAX_PROGRESS_LENGTH];
	static int last_str_len;

	#if DBG_DUMP_DATA_BLOCK
	/* when enable dumping data, disable show progress. */
	return;
	#endif

	if (state == SHOW_PROGRESS_INIT) {
		prt_info("\r");
		if (str != NULL)
			prt_info("%s", str);
		last_str_len = 0;

		return;
	} else if (state == SHOW_PROGRESS_EXIT) {
		prt_info("\n");
		if (str != NULL)
			prt_info("%s", str);
		last_str_len = 0;

		return;
	}

	if (state != SHOW_PROGRESS_CONT)
		return;

	fflush(stdout);
	for (i = 0; i < last_str_len; i++)
		prt_info("\b");

	percent = (percent < 0) ? 0 : ((percent > 100) ? 100 : percent);
	term_width = get_terminal_width();
	progress_count = percent * (term_width - 10) / 100;
	memset(buf, ' ', term_width);
	buf[0] = '|';
	memset(&buf[1], '-', progress_count);
	buf[term_width - 9] = '|';
	sprintf(&buf[term_width - 7], "%3d", percent);
	buf[term_width - 4] = '%';
	buf[term_width - 2] = '|';
	buf[term_width - 1] = '\0';

	prt_info("%s", buf);
	last_str_len = strlen(buf);
	fflush(stdout);
}

/*
 * sleep "msec" milliseconds.
 */
void msleep(unsigned int msec)
{
	struct timespec ts;
	int rc;

	ts.tv_sec = msec / 1000;
	ts.tv_nsec = (msec - (ts.tv_sec * 1000)) * 1000000;
	do {
		rc = nanosleep(&ts, &ts);
	} while (rc == -1 && errno == EINTR);
}

#define TIMER_START 0
#define TIMER_STOP 1
void calculate_duration_time(int start_stop, const char *str)
{
	static struct timeval last;
	struct timeval tv;
	struct timeval duration;

	gettimeofday(&tv, NULL);

	if (start_stop == TIMER_START) {
		last = tv;
		return;
	}

	if (start_stop != TIMER_STOP)
		return;

	timersub(&tv, &last, &duration);
	prt_info("%s: %ld.%06ld seconds\n", str ?: "Duration time",
			duration.tv_sec, duration.tv_usec);
	last = tv;
}

unsigned char cyapa_calculate_checksum(unsigned char *buf, int count)
{
	int i;
	unsigned char checksum = 0;

	for (i = 0; i < count; i++)
		checksum += buf[i];

	return checksum;
}

int cyapa_read_reg(int fd, unsigned int offset, int bytes, unsigned char *buf)
{
	int ret;
	int tries = 3;

	do {
		ret = (int)lseek(fd, (off_t)offset, SEEK_SET);
		if (ret < 0)
			continue;

		ret = (int)read(fd, buf, (ssize_t)bytes);
		if (ret == bytes)
			break;
	} while (tries--);

	if (tries < 0)
		return -1;

	return ret;
}

int cyapa_write_reg(int fd, unsigned char *buf, unsigned int offset, int bytes)
{
	int ret;
	int tries = 3;

	do {
		ret = (int)lseek(fd, (off_t)offset, SEEK_SET);
		if (ret < 0)
			continue;

		ret = (int)write(fd, buf, (ssize_t)bytes);
		if (ret == bytes)
			break;
	} while (tries--);

	if (tries < 0)
		return -1;

	return ret;
}

int open_trackpad_dev(struct args *args)
{
	char dev_name[64];

	sprintf(dev_name, "/dev/%s", CYAPA_MISC_NAME);
	args->fd_dev = open(dev_name, O_RDWR);
	if (args->fd_dev < 0) {
		prt_err("Cannot open device %s:\n", dev_name);
		perror(NULL);
		return -EFAULT;
	}

	if (lseek(args->fd_dev, 0, SEEK_SET) < 0) {
		prt_err("Cannot access device %s\n", dev_name);
		perror(NULL);
		close(args->fd_dev);
		return -errno;
	}
	return 0;
}

int open_new_fw_image(struct args *args)
{
	args->fd_new_fw = open(args->new_fw_image, O_RDONLY);
	if (args->fd_new_fw < 0) {
		prt_err("Cannot open firmware image %s\n", args->new_fw_image);
		perror(NULL);
		return -EFAULT;
	}
	return 0;
}

int open_bak_fw_image(struct args *args)
{
	args->fd_bak_fw = open(args->bak_fw_image,
			       O_WRONLY | O_CREAT | O_TRUNC,
			       S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
	if (args->fd_bak_fw < 0) {
		prt_err("Cannot open backup file %s\n", args->bak_fw_image);
		perror(NULL);
		return -EFAULT;
	}
	return 0;
}

int cyapa_fw_image_check(struct args *args)
{
	int ret;
	int tries = 3;
	int ret_bytes;
	unsigned int offset = 0;
	unsigned char buf[64];
	int fd = args->fd_new_fw;
	const char iic_identify[] = "00 00 FF 38 00 01 02 03 04 05 06 07";

	ret_bytes = 0;
	while ((ret_bytes != 64) && (tries-- > 0)) {
		ret = (int)lseek(fd, (off_t)offset, SEEK_SET);
		if (ret != (int)offset)
			continue;

		memset(buf, 0, sizeof(buf));
		ret_bytes = (int)read(fd, buf, 64);
	}
	if (tries < 0)
		return -1;

	if (args->file_type == FILE_TYPE_BIN) {
		if (buf[0x28] != 0xC0 || buf[0x29] != 0xC1 || buf[0x2A] != 0xC2)
			return -2;
	} else {
		if (strncmp(iic_identify, (const char *)&buf[5],
			strlen(iic_identify)))
			return -3;
	}

	/* reset file cursor to the start of the image file. */
	tries = 3;
	do {
		if (lseek(args->fd_new_fw, (off_t)offset, SEEK_SET) < 0)
			continue;
		else
			break;
	} while (tries--);
	if (tries < 0)
		return -4;

	return 0;
}

int cyapa_get_trackpad_run_mode(int fd,
		struct cyapa_trackpad_run_mode *run_mode)
{
	struct cyapa_misc_ioctl_data ioctl_data;

	memset(&ioctl_data, 0, sizeof(struct cyapa_misc_ioctl_data));
	ioctl_data.buf = (__u8 *)run_mode;
	ioctl_data.len = sizeof(struct cyapa_trackpad_run_mode);

	if (ioctl(fd, CYAPA_GET_TRACKPAD_RUN_MODE, &ioctl_data) < 0) {
		prt_dbg("Failed to get trackpad run mode state, %d\n", -errno);
		return -errno;
	}

	prt_dbg("GET: run_mode = %d, bootloader_state = %d\n",
		run_mode->run_mode, run_mode->bootloader_state);

	return 0;
}

int cyapa_set_trackpad_run_mode(int fd,
		struct cyapa_trackpad_run_mode *run_mode)
{
	struct cyapa_misc_ioctl_data ioctl_data;

	memset(&ioctl_data, 0, sizeof(struct cyapa_misc_ioctl_data));
	ioctl_data.buf = (__u8 *)run_mode;
	ioctl_data.len = sizeof(struct cyapa_trackpad_run_mode);

	if (ioctl(fd, CYAYA_SEND_MODE_SWITCH_CMD, &ioctl_data) < 0) {
		prt_dbg("failed to set trackpad device run mode state, %d\n",
			--errno);
		return -errno;
	}

	prt_dbg("SET: run_mode = %d, bootloader_state = %d\n",
		run_mode->run_mode, run_mode->bootloader_state);

	return 0;
}

void cyapa_recovery_bootload_header(struct args *args)
{
	unsigned char cmd[] = {0x00, 0xFF, 0x3C,
		0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
		0x07, 0x80};

	cyapa_write_reg(args->fd_dev, cmd, 0, sizeof(cmd));
	msleep(10);
}

int cyapa_set_bootloader_idle_mode(struct args *args)
{
	int tries = 3;
	int fd = args->fd_dev;
	struct cyapa_trackpad_run_mode run_mode;

	while (tries-- > 0) {
		memset(&run_mode, 0, sizeof(struct cyapa_trackpad_run_mode));
		if (cyapa_get_trackpad_run_mode(fd, &run_mode) < 0)
			continue;

		if ((run_mode.bootloader_state == CYAPA_BOOTLOADER_IDLE_STATE)
			&& (run_mode.run_mode == CYAPA_BOOTLOADER_MODE))
			break;  /* already in correct state. */

		if (run_mode.run_mode == CYAPA_OPERATIONAL_MODE) {
			run_mode.rev_cmd = CYAPA_CMD_APP_TO_IDLE;
			if (cyapa_set_trackpad_run_mode(fd, &run_mode) < 0)
				continue;

			msleep(300);
		} else if ((run_mode.run_mode == CYAPA_BOOTLOADER_MODE)
			&& (run_mode.bootloader_state ==
				CYAPA_BOOTLOADER_ACTIVE_STATE)) {
			run_mode.rev_cmd = CYAPA_CMD_ACTIVE_TO_IDLE;
			if (cyapa_set_trackpad_run_mode(fd, &run_mode) < 0)
				continue;

			msleep(300);
		} else {
			/*
			 * unknown trackpad states.
			 * try to recovery bootloader header registers.
			 */
			cyapa_recovery_bootload_header(args);
			continue;
		}
	}

	if (tries < 0)
		return -1;

	return 0;
}

int cyapa_set_bootloader_active_mode(struct args *args)
{
	int tries = 3;
	int fd = args->fd_dev;
	struct cyapa_trackpad_run_mode run_mode;

	while (tries-- > 0) {
		memset(&run_mode, 0, sizeof(struct cyapa_trackpad_run_mode));
		if (cyapa_get_trackpad_run_mode(fd, &run_mode) < 0)
			continue;

		if ((run_mode.bootloader_state == CYAPA_BOOTLOADER_ACTIVE_STATE)
			&& (run_mode.run_mode == CYAPA_BOOTLOADER_MODE))
			break;  /* already in correct state. */

		if (run_mode.run_mode == CYAPA_OPERATIONAL_MODE) {
			if (cyapa_set_bootloader_idle_mode(args) < 0)
				continue;

		} else if ((run_mode.run_mode == CYAPA_BOOTLOADER_MODE)
			&& (run_mode.bootloader_state ==
				CYAPA_BOOTLOADER_IDLE_STATE)) {
			run_mode.rev_cmd = CYAPA_CMD_IDLE_TO_ACTIVE;
			if (cyapa_set_trackpad_run_mode(fd, &run_mode) < 0)
				continue;

			msleep(12000);
			break;
		} else {
			/*
			 * unknown trackpad states.
			 * try to recovery bootloader header registers.
			 */
			cyapa_recovery_bootload_header(args);
			continue;
		}
	}

	if (tries < 0)
		return -1;

	return 0;
}

int cyapa_set_app_operational_mode(struct args *args)
{
	int tries = 3;
	int fd = args->fd_dev;
	struct cyapa_trackpad_run_mode run_mode;


	while (tries-- > 0) {
		memset(&run_mode, 0, sizeof(struct cyapa_trackpad_run_mode));
		if (cyapa_get_trackpad_run_mode(fd, &run_mode) < 0)
			continue;

		if (run_mode.run_mode == CYAPA_OPERATIONAL_MODE)
			break;  /* already in correct state. */

		if ((run_mode.run_mode == CYAPA_BOOTLOADER_MODE)
			&& (run_mode.bootloader_state ==
				CYAPA_BOOTLOADER_IDLE_STATE)) {
			run_mode.rev_cmd = CYAPA_CMD_IDLE_TO_APP;
			if (cyapa_set_trackpad_run_mode(fd, &run_mode) < 0)
				continue;

			msleep(300);
		} else if ((run_mode.run_mode == CYAPA_BOOTLOADER_MODE)
			&& (run_mode.bootloader_state ==
				CYAPA_BOOTLOADER_ACTIVE_STATE)) {
			run_mode.rev_cmd = CYAPA_CMD_ACTIVE_TO_IDLE;
			if (cyapa_set_trackpad_run_mode(fd, &run_mode) < 0)
				continue;

			msleep(300);
		} else {
			/*
			 * unknown trackpad states.
			 * try to recovery bootloader header registers.
			 */
			cyapa_recovery_bootload_header(args);
			continue;
		}
	}

	if (tries < 0)
		return -1;

	return 0;
}

int cyapa_get_firmware_version(struct args *args,
		struct cyapa_firmware_ver *fw_version)
{
	int ret = 0;
	int tries = 3;
	int fd = args->fd_dev;
	struct cyapa_misc_ioctl_data ioctl_data;
	struct cyapa_trackpad_run_mode run_mode;
	unsigned char buf[32];

	do {
		memset(&run_mode, 0, sizeof(struct cyapa_trackpad_run_mode));
		ret = cyapa_get_trackpad_run_mode(fd, &run_mode);
		if (ret == 0)
			break;

		cyapa_recovery_bootload_header(args);
	} while (tries--);

	if (ret < 0)
		goto error;

	if (run_mode.run_mode == CYAPA_OPERATIONAL_MODE) {
		memset(&ioctl_data, 0, sizeof(struct cyapa_misc_ioctl_data));
		ioctl_data.buf = (__u8 *)fw_version;
		ioctl_data.len = sizeof(struct cyapa_firmware_ver);

		if (ioctl(args->fd_dev,
			CYAPA_GET_FIRMWARE_VER, &ioctl_data) < 0)
			goto error;

		return 0;
	}

	/*
	 * firmware working in bootload mode.
	 * try to get firmware version from bootload header.
	 */
	memset(buf, 0, sizeof(buf));
	ret = cyapa_read_reg(fd, 0, 16, buf);
	if ((ret < 0) || ((buf[0x01] & 0x10) != 0x10))
		goto error;

	/* have valid bootload head and firmware version. */
	if ((buf[0x0D] == 0xC0) && (buf[0x0E] == 0xC1) && (buf[0x0F] == 0xC2)) {
		fw_version->major_ver = buf[0x0B];
		fw_version->minor_ver = buf[0x0C];
	} else
		goto error;

	return 0;

error:
	fw_version->major_ver = 0;
	fw_version->minor_ver = 0;
	prt_warn("Unknown trackpad device firmware version.\n");

	return ret;
}

int cyapa_get_protocol_version(struct args *args)
{
	struct cyapa_misc_ioctl_data ioctl_data;
	struct cyapa_protocol_ver protocol_gen;

	memset(&ioctl_data, 0, sizeof(struct cyapa_misc_ioctl_data));
	ioctl_data.buf = (__u8 *)&protocol_gen;
	ioctl_data.len = sizeof(struct cyapa_protocol_ver);

	if (ioctl(args->fd_dev, CYAPA_GET_PROTOCOL_VER, &ioctl_data) < 0)
		return 0;

	return (int)protocol_gen.protocol_gen;
}

/*
 * Read a block of firmware bytes from trackpad and store them in the supplied
 * buffer.
 */
int cyapa_read_fw_block(int fd, unsigned short offset, unsigned char *buf,
			int len)
{
	int tries = 3;
	unsigned char cmd_str[] = {0x00, 0xFF, 0x3C,
		0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
	unsigned char read_cmd[sizeof(cmd_str) + 2];
	unsigned char read_buf[len];
	unsigned char verify_buf[len];

	if (len > 16) {
		prt_err("Can't read more then 16 bytes firmware each time.\n");
		return -1;
	}

	memcpy(read_cmd, cmd_str, sizeof(cmd_str));
	/* big endian for the value of offset when sending command. */
	read_cmd[sizeof(cmd_str)] = offset >> 8;
	read_cmd[sizeof(cmd_str) + 1] = offset;

	cyapa_dump_data_block("TP: read cmd", read_cmd, 0, sizeof(read_cmd));

	/*
	 * since trackpad doesn't protect i2c data with a CRC,
	 * verify data by reading it twice.
	 */
	while (tries-- > 0) {
		cyapa_write_reg(fd, read_cmd, 0, sizeof(read_cmd));
		msleep(1);
		cyapa_read_reg(fd, 16, len, read_buf);

		cyapa_write_reg(fd, read_cmd, 0, sizeof(read_cmd));
		msleep(1);
		cyapa_read_reg(fd, 16, len, verify_buf);

		if (!memcmp(read_buf, verify_buf, len))
			break;
	}

	if (tries < 0)
		return -2;

	memcpy(buf, read_buf, len);

	cyapa_dump_data_block("TP: read data", buf, offset, len);
	return len;
}

/*
 * Some values read from the trackpad are volatile and must be restored to
 * defaults before being used to write back as firmware.
 *
 * Assumes: sizeof buffer >= CYAPA_FW_SIZE
 */
void cyapa_fixup_fw_buffer(unsigned char* buf)
{
	/*
	 * Set IDAC regions in the image to a default pattern.
	 * The values read from the device are dynamically generated at runtime
	 * during calibration, and should be ignored. Setting these regions
	 * to the pattern will ensure the resulting fw backup image has the
	 * correct checksum.
	 */
	memset(&buf[CYAPA_FW_TO_BIN(CYAPA_IDAC_0_START)], CYAPA_IDAC_0_DEF,
	       CYAPA_IDAC_0_SIZE);
	memset(&buf[CYAPA_FW_TO_BIN(CYAPA_IDAC_1_START)], CYAPA_IDAC_1_DEF,
	       CYAPA_IDAC_1_SIZE);
	memset(&buf[CYAPA_FW_TO_BIN(CYAPA_IDAC_2_START)], CYAPA_IDAC_2_DEF,
	       CYAPA_IDAC_2_SIZE);
}

int cyapa_backup_fw_from_trackpad(struct args *args)
{
	int i;
	int ret;
	int offset;
	int len;
	int total;

	ret = cyapa_set_bootloader_idle_mode(args);
	if (ret < 0) {
		prt_err("Failed to reset trackpad device, unable switch to "
			"firmware bootloader idle state, %d.\n",
			ret);
		return -1;
	}

	calculate_duration_time(TIMER_START, NULL);
	show_progress(0, SHOW_PROGRESS_INIT,
		"Backup firmware from trackpad device in progress:\n");
	show_progress(0, SHOW_PROGRESS_CONT, NULL);

	total = CYAPA_FW_SIZE;
	offset = CYAPA_FW_OFFSET_START;
	len = CYAPA_BAK_READ_BLOCK_LEN;
	for (i = 0; i < total; i += len) {
		ret = cyapa_read_fw_block(args->fd_dev, offset + i,
					  &fw_buf[i], len);
		if (ret < 0) {
			prt_err("Failed to read firmware image, %d.\n", ret);
			return -2;
		}

		show_progress(100 * (i+1) / total, SHOW_PROGRESS_CONT, NULL);
	}

	cyapa_fixup_fw_buffer(fw_buf);

	/* store read data to output file. */
	ret = write(args->fd_bak_fw, fw_buf, total);
	if (ret != total) {
		prt_err("Failed to write backup firmware image.\n");
		perror(NULL);
		return -3;
	}

	show_progress(0, SHOW_PROGRESS_EXIT,
		"Backup firmware from trackpad device done.\n");
	calculate_duration_time(TIMER_STOP,
		"Backup firmware form trackpad device duration time");

	return total;
}

/*
 * routines for updating trackpad firmware from .iic firmware image file.
 */
int string_to_hex(const char *str)
{
	int i, j;
	int len = strlen(str);
	char c = 0;
	int d = 0;
	int hex = 0;

	for (j = 0, i = len - 1; i >= 0; j++, i--) {
		c = tolower(*(str + i));
		if ((c >= '0') && (c <= '9'))
			d = c - '0';
		else
			d = c - 'a' + 10;
		hex += (d << 4 * j);
	}

	return hex;
}

int cyapa_iic_cmd_to_data(char *iic_buf, int *cmd_op,
		unsigned char *outbuf, int *cmd_data_len)
{
	int i = 0;
	int j = 0;
	char *p = NULL;
	char *tmpp = NULL;
	char hexstr[4];
	char buf[128];

	memset(buf, 0, sizeof(buf));
	strcpy(buf, iic_buf);
	p = buf;

	/* skip white-space if exists for the head of the string. */
	while (*p == ' ')
		p++;

	/* delay specific time command. */
	if (*p == '[' && !strncmp((p + 1), "delay=", 6)) {
		*cmd_op = CYAPA_IIC_CMD_OP_DELAY;

		tmpp = strrchr(p, ']');
		*tmpp = '\0';
		p = strchr(p, '=') + 1;
		*(int *)outbuf = atoi(p);
		*cmd_data_len = sizeof(int);

		return 0;
	}

	if (*p == 'r')
		*cmd_op = CYAPA_IIC_CMD_OP_READ;
	else if (*p == 'w')
		*cmd_op = CYAPA_IIC_CMD_OP_WRITE;
	else {
		/*
		 * this iis command line doesn't contain valid command data.
		 * skip this command line.
		 */
		*cmd_op = CYAPA_IIC_CMD_OP_NONE;
		*cmd_data_len = 0;
		return 0;
	}
	/* skip white-space. */
	while (*(++p) == ' ')
		;

	/* parse command data. */
	while (*p != 'p') {
		if (*p == ' ') {
			p++;
			continue;
		}

		i = 0;
		memset(hexstr, 0, sizeof(hexstr));
		while (*p != ' ')
			hexstr[i++] = *(p++);
		if (!strcmp(hexstr, "x"))
			j++;
		else
			outbuf[j++] = string_to_hex(hexstr);
	}
	*cmd_data_len = j;

	return 0;
}

int cyapa_send_update_cmds(struct args *args,
		struct cmds_update_block *iic_cmds)
{
	int i;
	int ret = 0;
	int tries = 3;
	int fd = args->fd_dev;
	int cmd_op;
	unsigned int offset;
	int cmd_len;
	unsigned char delay = 100;
	unsigned char *cmd_buf = NULL;
	unsigned char status[32];

	if (iic_cmds->valid_cmds <= 0)
		return 0;

	do {
		for (i = 0; i < iic_cmds->valid_cmds; i++) {
			/* byte 0: cmd_op, read/write/delay*/
			cmd_op = iic_cmds->cmds[i][0];

			if (cmd_op == CYAPA_IIC_CMD_OP_WRITE) {
				/*
				 * command format for read/write:
				 * byte 0: cmd_op, read/write;
				 * byte 1: total cmd len;
				 * byte 2: i2c device addr;
				 * byte 3: i2c register map offset;
				 * byte 4 - ... : cmd data.
				 */
				cmd_len = iic_cmds->cmds[i][1] - 4;
				offset = iic_cmds->cmds[i][3];
				cmd_buf = &iic_cmds->cmds[i][4];
				cyapa_dump_data_block("CYAPA_IIC_CMD_OP_WRITE",
					cmd_buf, offset, cmd_len);
				ret = cyapa_write_reg(fd, cmd_buf,
					offset, cmd_len);
				if (ret < 0)
					break;
			} else if (cmd_op == CYAPA_IIC_CMD_OP_DELAY) {
				/*
				 * byte 0: cmd_op, delay;
				 * byte 1: total cmd len;
				 * byte 2 - byte 5: time in milliseconds, int.
				 */
				delay = iic_cmds->cmds[i][2];
				cyapa_dump_delay_time("CYAPA_IIC_CMD_OP_DELAY",
					delay);
				msleep(delay);
			} else if (cmd_op == CYAPA_IIC_CMD_OP_READ) {
				/*
				 * byte 0: cmd_op, read;
				 * byte 1: total cmd len;
				 * byte 2: i2c device addr.
				 * byte 3 - ... : total bytes number indicates
				 *               how many bytes should be read.
				 */
				memset(status, 0, sizeof(status));
				cmd_len = iic_cmds->cmds[i][1] - 3;
				ret = cyapa_read_reg(fd, 0, cmd_len, status);
				cyapa_dump_data_block("CYAPA_IIC_CMD_OP_READ",
					status, 0, cmd_len);
				if ((ret < 0) || (status[2] != 0x20))
					break;  /* tries to write this block. */
			}
		}

		if (i == iic_cmds->valid_cmds)
			break;
	} while (tries--);

	if (tries < 0)
		return -1;

	return 0;
}

int is_bootloader_terminal_cmd(struct cmds_update_block *iic_cmds)
{
	int i;
	unsigned char terminal_bootloader_cmd[] = {0x00, 0xFF, 0x3B,
		0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};

	if (iic_cmds->valid_cmds != 1)
		return 0;

	for (i = 0; i < sizeof(terminal_bootloader_cmd); i++) {
		if (iic_cmds->cmds[0][i+4] != terminal_bootloader_cmd[i])
			break;
	}

	return (i == sizeof(terminal_bootloader_cmd)) ? 1 : 0;
}

#define IIC_CHECKSUM_START_BLOCK 0x001E
#define IIC_CHECKSUM_END_BLOCK 0x001F
#define IIC_APP_START_BLOCK  0x0020
#define IIC_APP_END_BLOCK 0x01FF
int cyapa_update_fw_image_from_iic(struct args *args)
{
	int ret;
	int block_index;
	int total_blocks;
	int fd = args->fd_new_fw;
	char iic_buf[128];
	int cmd_op;
	unsigned char cmd_buf[CYAPA_FW_BLOCK_LEN];
	int cmd_data_len;
	FILE *fp = fdopen(fd, "r");
	char *retp = NULL;
	struct cmds_update_block iic_cmds;

	if (fp == NULL) {
		fp = fopen(args->new_fw_image, "r");
		if (fp == NULL) {
			prt_err("Failed to open new firmware image file, %d\n",
				-errno);
			ret = -errno;
			goto error;
		}
	}

	calculate_duration_time(TIMER_START, NULL);
	total_blocks = (IIC_APP_END_BLOCK - IIC_APP_START_BLOCK + 1) +
		(IIC_CHECKSUM_END_BLOCK - IIC_CHECKSUM_START_BLOCK + 1);
	show_progress(0, SHOW_PROGRESS_INIT,
		"Update trackpad firmware from .iic file in progress:\n");
	show_progress(0, SHOW_PROGRESS_CONT, NULL);

	/* updating firmware. */
	memset(&iic_cmds, 0, sizeof(struct cmds_update_block));
	while (1) {
		/* read a command line from .iic file. */
		memset(iic_buf, 0, sizeof(iic_buf));
		retp = fgets(iic_buf, sizeof(iic_buf), fp);
		/* TODO(djkurtz): Why forcing errno to 0?!? */
		errno = 0;
		if (retp == NULL) {
			if (errno == 0) {
				show_progress(0, SHOW_PROGRESS_EXIT,
					"Update firmware from .iic"
					" file done.\n");
				calculate_duration_time(TIMER_STOP,
					"Update firmware from .iic file"
					" duration time");
				ret = 0;
				break;
			} else {
				prt_err("Failed to read next command line"
					" from .iic file, %d.\n", -errno);
				ret = -errno;
				goto error;
			}
		}

		/* convert .iic command line to i2c data. */
		memset(cmd_buf, 0, sizeof(cmd_buf));
		cyapa_iic_cmd_to_data(iic_buf, &cmd_op, cmd_buf, &cmd_data_len);

		if (cmd_op == CYAPA_IIC_CMD_OP_NONE)
			continue;

		iic_cmds.cmds[iic_cmds.valid_cmds][0] = (unsigned char)cmd_op;
		iic_cmds.cmds[iic_cmds.valid_cmds][1] =
			(unsigned char)(cmd_data_len + 2);
		memcpy(&iic_cmds.cmds[iic_cmds.valid_cmds][2],
			cmd_buf, (cmd_data_len + 2));
		iic_cmds.valid_cmds++;

		/* update new firmware block data to trackpad device. */
		if ((cmd_op == CYAPA_IIC_CMD_OP_READ)
			|| (is_bootloader_terminal_cmd(&iic_cmds))) {
			ret = cyapa_send_update_cmds(args, &iic_cmds);
			if (ret < 0) {
				prt_err("Failed to write new firmware's .iic"
					" block data, %d.\n", ret);
				goto error;
			}

			/* show new firmware update progress. */
			if (iic_cmds.valid_cmds > 5) {
				block_index = iic_cmds.cmds[0][15] << 8 |
					iic_cmds.cmds[0][16];
				if (block_index == IIC_CHECKSUM_START_BLOCK)
					block_index = IIC_APP_END_BLOCK + 1;
				else if (block_index == IIC_CHECKSUM_END_BLOCK)
					block_index = IIC_APP_END_BLOCK + 2;

				show_progress(((block_index -
						IIC_APP_START_BLOCK + 1) * 100 /
						total_blocks),
					SHOW_PROGRESS_CONT,
					NULL);
			}

			memset(&iic_cmds, 0, sizeof(struct cmds_update_block));
		}
	}

error:
	fclose(fp);

	return ret;
}

/*
 * routines for updating trackpad firmware from .bin firmware image file.
 */
int cyapa_read_bin_fw_image(struct args *args,
		unsigned short offset, unsigned char *buf, int len)
{
	int ret = 0;
	int read_bytes = 0;
	int fd = args->fd_new_fw;
	unsigned short read_offset;
	int tries = 3;

	if (args->file_type != FILE_TYPE_BIN)
		return -1;

	read_offset = offset - CYAPA_FW_OFFSET_START;
	while ((read_bytes != len) && (tries-- > 0)) {
		ret = (int)lseek(fd, (off_t)read_offset, SEEK_SET);
		if (ret != (int)read_offset)
			continue;

		read_bytes = (int)read(fd, buf, (ssize_t)len);
	}

	if (tries < 0)
		return -2;

	return 0;
}

int cyapa_write_fw_image_block(struct args *args,
		unsigned short offset, unsigned char *buf, int len)
{
	int ret = 0;
	int tries = 3;
	int fd = args->fd_dev;
	unsigned char status[3];
	unsigned char cmd_buf[32];
	unsigned char block_offset;
	unsigned char block_buf[78];
	unsigned char *p;
	int left_len;
	int cmd_len;
	unsigned short block_index;

	memset(block_buf, 0, sizeof(block_buf));
	/* set write command and security key bytes. */
	block_buf[0] = 0xFF;
	block_buf[1] = 0x39;
	block_buf[2] = 0x00;
	block_buf[3] = 0x01;
	block_buf[4] = 0x02;
	block_buf[5] = 0x03;
	block_buf[6] = 0x04;
	block_buf[7] = 0x05;
	block_buf[8] = 0x06;
	block_buf[9] = 0x07;
	/* block index is sent big endian. */
	block_index = (unsigned short)(offset / CYAPA_FW_BLOCK_LEN);
	block_buf[10] = block_index >> 8;
	block_buf[11] = block_index;
	memcpy(&block_buf[12], buf, len);
	/* checksum for block data and whole command. */
	block_buf[76] = cyapa_calculate_checksum(&block_buf[12], len);
	block_buf[77] = cyapa_calculate_checksum(&block_buf[0], 77);

	do {
		p = block_buf;
		left_len = sizeof(block_buf);
		block_offset = 0;
		while (left_len > 0) {
			cmd_len = (left_len >= 16) ? 16 : left_len;

			memset(cmd_buf, 0, sizeof(cmd_buf));
			cmd_buf[0] = block_offset;
			memcpy(&cmd_buf[1], p, cmd_len);

			p += cmd_len;
			block_offset += cmd_len;
			left_len -= cmd_len;

			/* write block data to trackpad device. */
			cyapa_dump_data_block("CYAPA_WRITE_BLOCK_DATA",
				cmd_buf, offset, (cmd_len + 1));
			if (cyapa_write_reg(fd, cmd_buf, 0, (cmd_len + 1)) < 0)
				break;
		}

		/* wait write command finished by trackpad device. */
		msleep(100);
		cyapa_dump_delay_time("CYAPA_WRITE_BLOCK_DELAY", 100);

		if (left_len > 0)  /* write failed, tries again. */
			continue;

		/* reset device pointer to offset 0. */
		memset(cmd_buf, 0, sizeof(cmd_buf));
		if (cyapa_write_reg(fd, cmd_buf, 0, 0) < 0)
			prt_warn("Failed to reset trackpad device pointer.\n");

		/* check block write command result status. */
		memset(status, 0, sizeof(status));
		ret = cyapa_read_reg(fd, 0, 3, status);
		cyapa_dump_data_block("CYAPA_WRITE_STATUS_STATUS",
			status, 0, 3);
		if ((ret < 0) || (status[2] != 0x20))
			continue;

		/* block data written successfully. */
		break;
	} while (tries--);

	if (tries < 0)
		return -1;

	return 0;
}

int cyapa_update_part_fw_image(struct args *args,
		unsigned short start, unsigned end, int *blocks_written)
{
	int blocks;
	int ret = 0;
	int total_blocks;
	unsigned short offset;
	unsigned char buf[CYAPA_FW_BLOCK_LEN];

	total_blocks = (CYAPA_FW_OFFSET_END - CYAPA_FW_OFFSET_START + 1) /
			CYAPA_FW_BLOCK_LEN;
	blocks = 0;
	offset = start + blocks * CYAPA_FW_BLOCK_LEN;
	while ((offset + CYAPA_FW_BLOCK_LEN - 1) <= end) {
		/* read data block from firmware image file. */
		memset(buf, 0, sizeof(buf));
		ret = cyapa_read_bin_fw_image(args, offset, buf,
					      CYAPA_FW_BLOCK_LEN);
		if (ret < 0) {
			prt_err("Failed to read %d bytes block data at 0x%04x"
				" from .bin firmware image file, %d\n",
				CYAPA_FW_BLOCK_LEN, offset, ret);
			return ret;
		}

		/* write firmware data block to trackpad device. */
		ret = cyapa_write_fw_image_block(args, offset,
				buf, CYAPA_FW_BLOCK_LEN);
		if (ret < 0) {
			prt_err("Failed to write an image block data"
				" to trackpad device, %d\n", ret);
			return ret;
		}

		/* update pointers. */
		blocks++;
		offset = start + blocks * CYAPA_FW_BLOCK_LEN;

		++(*blocks_written);
		show_progress(((*blocks_written) * 100 / total_blocks),
			SHOW_PROGRESS_CONT, NULL);
	}

	return blocks;
}

int cyapa_update_fw_image_from_bin(struct args *args)
{
	int ret;
	int blocks_written = 0;

	calculate_duration_time(TIMER_START, NULL);
	show_progress(0, SHOW_PROGRESS_INIT,
		"Update trackpad firmware from .bin file in progress:\n");
	show_progress(0, SHOW_PROGRESS_CONT, NULL);

	/* switch firmware working state to bootloader active state. */
	ret = cyapa_set_bootloader_active_mode(args);
	if (ret < 0) {
		prt_err("Failed switching firmware working state"
			" to bootloader active state.\n");
		goto error;
	}

	/*
	 * update firmware image to trackpad device.
	 * by default, should burn checksum blocks in the last step.
	 */
	ret = cyapa_update_part_fw_image(args, (CYAPA_FW_CHECKSUM_END + 1),
			CYAPA_FW_OFFSET_END, &blocks_written);
	if (ret < 0) {
		prt_err("Failed to write firmware image blocks to"
			" trackpad device.\n");
		goto error;
	}

	ret = cyapa_update_part_fw_image(args, CYAPA_FW_OFFSET_START,
			CYAPA_FW_CHECKSUM_END, &blocks_written);
	if (ret < 0) {
		prt_err("Failed to write checksum data blocks"
			" to trackpad device.\n");
		goto error;
	}

	show_progress(0, SHOW_PROGRESS_EXIT,
		"Update firmware from .bin image file done.\n");
	calculate_duration_time(TIMER_STOP,
		"Update firmware from .bin image file duration time");
error:
	/* set firmware back to bootloader idle mode working state. */
	if (cyapa_set_bootloader_idle_mode(args) < 0) {
		prt_warn("Failed to switch firmware working state"
			" to bootloader idle state.\n");
	}

	return ret;
}

int cyapa_update_firmware(struct args *args)
{
	int ret;

	if (args->file_type == FILE_TYPE_IIC)
		ret = cyapa_update_fw_image_from_iic(args);
	else
		ret = cyapa_update_fw_image_from_bin(args);

	return ret;
}

int check_run_as_root(char *name)
{
	const char *program;

	if (getuid() == 0)
		return 0;

	program = basename(name) ?: DEFAULT_PROGRAM_NAME;

	prt_warn("\"%s\" must be run as root.\n", program);
	prt_warn("e.g.:\n");
	prt_warn("    sudo %s new-firmware [options]\n", program);
	prt_warn("Please run \"%s -h\" for more information.\n", program);

	return -1;
}

static bool prompt(const char *intro, const char *Q, const char *Y,
		   const char *N)
{
	int c;
	prt_warn("%s", intro);
	do {
		prt_info("%s", Q);
		c = getchar();
		if (toupper(c) == 'Y') {
			prt_info("%s", Y);
			break;
		} else {
			prt_info("%s", N);
			return false;
		}
	} while (c != EOF);
	return true;
}

static bool warn_backup_fail(void)
{
	return prompt("Failed to open firmware backup file.\n",
		      "Continue update without backup? <Y/N>: ",
		      "Continuing firmware update...\n",
		      "Canceling firmware update.\n");
}

static size_t line_to_buf(char *line, unsigned char *buf, size_t len)
{
	size_t cnt = 0;
	unsigned char b;
	char *next;

	do {
		errno = 0;
		b = strtoul(line, &next, 16);
		if (line == next)
			break;
		line = next;
		buf[cnt++] = b;
	} while (cnt < len);

	return cnt;
}

const unsigned char wr_cmd[] = { 0xFF, 0x39, 0x00, 0x01, 0x02, 0x03, 0x04,
				 0x05, 0x06, 0x07 };

/*
 * Process a 'w' line from an iic file.
 *
 * 5 'w' lines, when taken together, form a 78-byte write fw block command,
 * with a 64-byte payload.
 */
static void proc_iic_w_line(const unsigned char *buf, size_t len)
{
	static unsigned char cmd[14 + CYAPA_FW_BLOCK_LEN] = { 0 };
	size_t offset;
	size_t cmd_len;
	ssize_t block;
	unsigned char csum;

	offset = buf[0];
	cmd_len = offset + len - 1;
	if (cmd_len > sizeof(cmd) || len == 0)
		return;

	memcpy(&cmd[offset], &buf[1], len - 1);

	/* Start processing only if command is a full 78 bytes. */
	if (cmd_len !=  sizeof(cmd))
		return;

	if (memcmp(cmd, wr_cmd, sizeof(wr_cmd)))
		return;

	/* Penultimate byte is crc of payload */
	csum = cyapa_calculate_checksum(&cmd[12], CYAPA_FW_BLOCK_LEN);
	if (csum != cmd[sizeof(cmd)-2])
		return;
	/* Last byte is crc of entire command */
	csum = cyapa_calculate_checksum(cmd, sizeof(cmd)-1);
	if (csum != cmd[sizeof(cmd)-1])
		return;

	block = ((cmd[10] << 8) | cmd[11]) - CYAPA_FW_START_BLOCK;
	if (block >= 0 && block < CYAPA_FW_BLOCK_COUNT)
		memcpy(&fw_buf[block * CYAPA_FW_BLOCK_LEN], &cmd[12],
		       CYAPA_FW_BLOCK_LEN);
}

/*
 * An iic file consists of a sequence of commands, each on a single text line.
 * The commands are 'r', 'w', or '[delay=X]'.
 *
 * Each write command has the following format:
 * w <addr> <line_offset>
 */
static int convert_iic(struct args *args)
{
	int ret;
	FILE *f_iic;
	FILE *f_bin;
	char line[256];
	unsigned char buf[20];
	size_t n;

	if (!args->new_fw_image || !args->bak_fw_image)
		return -EINVAL;
	f_iic = fopen(args->new_fw_image, "r");
	if (f_iic == NULL) {
		perror(NULL);
		return -EIO;
	}

	while (!feof(f_iic)) {
		char *r = fgets(line, sizeof(line), f_iic);
		if (r == NULL)
			break;

		if (ferror(f_iic)) {
			perror(NULL);
			ret = -1;
			goto close_iic;
		}

		/* Ignore lines that aren't part of a write command */
		if (strncmp(line, "w 67 00 ", 8))
			continue;

		/* Extract remaining hex-encoded bytes on line into buf */
		n = line_to_buf(&line[8], buf, sizeof(buf));

		/* Ignore "w 67 00 p" */
		if (n == 0)
			continue;

		proc_iic_w_line(buf, n);
	}

	f_bin = fopen(args->bak_fw_image, "wb");
	if (f_bin == NULL) {
		perror(NULL);
		ret = -EIO;
		goto close_iic;
	}

	printf("Writing %d bytes\n", CYAPA_FW_SIZE);
	ret = fwrite(fw_buf, 1, CYAPA_FW_SIZE, f_bin);
	if (ret != CYAPA_FW_SIZE) {
		perror(NULL);
		ret = -EIO;
		goto close_bin;
	}

	ret = 0;
close_bin:
	fclose(f_bin);
close_iic:
	fclose(f_iic);
	return ret;
}

int main(int argc, char **argv)
{
	int err_code = 0;
	int fd_temp = 0;
	struct args args = { 0 };
	struct cyapa_firmware_ver fw_version;
	int old_protocol = -1;
	int new_protocol = -1;

	/* only root priority user can execute this program. */
	if (check_run_as_root(argv[0]) < 0)
		exit(-1);

	/* parse input parameters. */
	err_code = check_input_args(argc, argv, &args);
	if (err_code == 2) {
		show_version_info(basename(argv[0]));
		exit(0);
	} else if (err_code == 1) {
		show_usage(basename(argv[0]));
		exit(0);
	}

	if (args.convert)
		return convert_iic(&args);

	/* open device and firmware image files. */
	if (open_trackpad_dev(&args)) {
		prt_err("unable to open trackpad device.\n");
		exit(-3);
	}

	if (args.new_fw_image && open_new_fw_image(&args)) {
		prt_err("unable to open new firmware file.\n");
		exit(-4);
	}

	if (args.backup_fw && open_bak_fw_image(&args) && !warn_backup_fail()) {
		prt_err("unable to open backup file.\n");
		exit(-5);
	}

	if (args.new_fw_image && cyapa_fw_image_check(&args) < 0) {
		prt_err("New firmware image file \"%s\" is invalid.\n",
			args.new_fw_image);
		prt_info("Please specify a valid new trackpad"
			" firmware image file.\n");
		exit(-6);
	}

	/* show firmware version before firmware updated. */
	memset(&fw_version, 0, sizeof(struct cyapa_firmware_ver));
	if (cyapa_get_firmware_version(&args, &fw_version) < 0) {
		err_code = -6;
		goto error;
	}
	old_protocol = cyapa_get_protocol_version(&args);
	prt_info("Firmware version before updated is: <%02d.%02d>,"
		" protocol version is: GEN%d.\n\n",
		fw_version.major_ver, fw_version.minor_ver, old_protocol);

	/* backup firmware image for trackpad device. */
	if (args.backup_fw) {
		if (cyapa_backup_fw_from_trackpad(&args) < 0) {
			if (warn_backup_fail())
				goto error;
		} else {
			prt_info("Backup firmware image from trackpad device"
				" to \"%s\" successfully.\n\n",
				args.bak_fw_image);
		}
	}

	/* If a new firmware file was provided, do update . */
	if (args.new_fw_image) {
		/* reset firmware working state to bootloader idle state. */
		if (cyapa_set_bootloader_idle_mode(&args) < 0) {
			prt_err("Failed to reset and switch firmware working"
				" state to bootloader idle state.\n");
			err_code = -8;
			goto error;
		}

		if (cyapa_update_firmware(&args) < 0) {
			prt_err("Update new firmware image %s failed.\n",
				args.new_fw_image);
			err_code = -9;

			if (args.backup_fw) {
				prt_info("Tries to recovery trackpad device to"
					" previously backed up firmware image.\n");
				fd_temp = args.fd_new_fw;
				args.fd_new_fw = args.fd_bak_fw;
				args.file_type = FILE_TYPE_BIN;
				if (cyapa_update_firmware(&args) < 0) {
					prt_err("Trackpad firmware recovery failed.\n");
					err_code = -5;
				} else {
					err_code = 0;
					prt_info("Successfully restored backed up"
						" trackpad firmware\n");
				}
				args.fd_new_fw = fd_temp;
			}

			if (err_code < 0)
				goto error;
		}
		prt_info("Successfully updated trackpad to new firmware image %s\n\n",
			args.new_fw_image);
	}

error:
	/* reset trackpad device to operational mode. */
	if (cyapa_set_app_operational_mode(&args) < 0) {
		prt_info("Failed to reset trackpad device to"
			" operational mode.\n");
		prt_info("Need to reboot system to recover trackpad device.\n");
	}

	/* show firmware version after firmware updated. */
	new_protocol = cyapa_get_protocol_version(&args);
	memset(&fw_version, 0, sizeof(struct cyapa_firmware_ver));
	if ((cyapa_get_firmware_version(&args, &fw_version) >= 0)
		&& (err_code == 0))
		prt_info("Firmware version after update is: v%02d.%02d,"
			" new protocol is: GEN%d\n",
			fw_version.major_ver,
			fw_version.minor_ver,
			new_protocol);

	if (args.fd_dev > 0)
		close(args.fd_dev);
	if (args.fd_new_fw > 0)
		close(args.fd_new_fw);
	if (args.fd_bak_fw > 0)
		close(args.fd_bak_fw);

	return err_code;
}
